<?php

// Copyright 2019 Hackware SpA <human@hackware.cl>
// "Hackware Web Services Core" is released under the MIT License terms.

namespace Hawese\Tests;

use Hawese\Core\TableModel;
use Laravel\Lumen\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;

class TableModelTest extends TestCase
{
    use DatabaseTransactions;

    protected function setUp(): void
    {
        parent::setUp();

        $this->dumbObj = new DumbTableModel();
    }

    public function testConstructorWithData()
    {
        $dumbObject = new DumbTableModel(['attr1' => 'something']);
        $this->assertSame('something', $dumbObject->attr1);
    }

    public function testConstructorWithCustomSetters()
    {
        $dumbObject = new DumbTableModel(['custom_setter' => 'never_set_value']);
        $this->assertSame('custom_setter_value', $dumbObject->custom_setter);
    }

    public function testConstructorWithoutCustomSetters()
    {
        $dumbObject = new DumbTableModel(['custom_setter' => 'set_value'], false);
        $this->assertSame('set_value', $dumbObject->custom_setter);
    }

    public function testEmptyConstructor()
    {
        $this->assertNull($this->dumbObj->attr1);
    }

    public function testAccesorWithCustomGetter()
    {
        $this->dumbObj->custom_getter = 'never_seen_value';
        $this->assertSame('custom_getter_value', $this->dumbObj->custom_getter);
    }

    public function testUndefinedAccesor()
    {
        $this->expectExceptionMessage('Undefined property via __get()');
        $this->dumbObj->nonexistent_attr;
    }

    public function testMutatorWithCustomSetter()
    {
        $this->dumbObj->custom_setter = 'never_set_value';
        $this->assertSame('custom_setter_value', $this->dumbObj->custom_setter);
    }

    public function testUndefinedMutator()
    {
        $this->expectExceptionMessage('Undefined property via __set()');
        $this->dumbObj->nonexistent_attr = 'any_value';
        $this->assertNull($this->dumbObj->nonexistent_attr);
    }

    // I probably could write a better test
    // $dumbObj->data is protected and I intend to keep it that way.
    public function testAccesorAndMutatorWithData()
    {
        $this->dumbObj->attr1 = 'something_else';
        $this->assertSame('something_else', $this->dumbObj->attr1);
    }

    public function testAutomagicallyUseCarbonOnCrudDates()
    {
        $this->dumbObj->created_at = '2019-01-01 00:00';
        $this->dumbObj->updated_at = '2019-01-01 01:00';
        $this->dumbObj->deleted_at = '2019-01-01';
        $this->dumbObj->else_at = '2019-01-01 00:00';

        foreach (['created_at', 'updated_at', 'deleted_at'] as $dateAttr) {
            $this->assertInstanceOf(
                Carbon::class,
                $this->dumbObj->{$dateAttr}
            );
        }

        $this->assertNotInstanceOf(
            Carbon::class,
            $this->dumbObj->else_at
        );
    }

    public function testIsset()
    {
        $this->assertFalse(isset($this->dumbObj->attr1));
        $this->dumbObj->attr1 = 'anything';
        $this->assertTrue(isset($this->dumbObj->attr1));
    }

    /* implements ArrayAccess */

    public function testOffsetExists()
    {
        $this->assertFalse(isset($this->dumbObj['attr1']));
        $this->dumbObj->attr1 = '';
        $this->assertTrue(isset($this->dumbObj['attr1']));
        $this->assertTrue(empty($this->dumbObj['attr1']));
        $this->dumbObj->attr1 = true;
        $this->assertFalse(empty($this->dumbObj['attr1']));
    }

    public function testOffsetGet()
    {
        $this->dumbObj->attr1 = 'value';
        $this->assertSame('value', $this->dumbObj['attr1']);
    }

    public function testOffsetSet()
    {
        $this->dumbObj['attr1'] = 'value';
        $this->assertSame('value', $this->dumbObj->attr1);
    }

    public function testOffsetUnset()
    {
        $this->dumbObj->attr1 = 'value';
        $this->assertNotNull($this->dumbObj->attr1);
        unset($this->dumbObj['attr1']);
        $this->assertNull($this->dumbObj->attr1);
    }

    /* /implements ArrayAccess */

    public function arrayableJsonSerializableDumbObj()
    {
        $otherDumbObj = new class extends TableModel {
            public static $table = 'other_dumbs';
            public static $attributes = [
                'id' => [],
                'foreign_id' => [],
                'created_at' => []
            ];
            public static $foreign_keys = [
                'foreign_id' => ForeignTableModel::class
            ];
        };

        $foreignObj = new ForeignTableModel();

        $otherDumbObj->foreign_id = $foreignObj->insert();
        $otherDumbObj->foreignObjectGetter('foreign'); // test nested

        $otherDumbObj->appendInstanceAttribute('other_attr');
        $otherDumbObj->other_attr = 'other_val';

        return $otherDumbObj;
    }

    // Implements Arrayable
    public function testToArray()
    {
        $otherDumbObj = $this->arrayableJsonSerializableDumbObj();
        $this->assertEqualsCanonicalizing(
            [
                'id' => null,
                'foreign_id' => $otherDumbObj->foreign->id,
                'foreign' => ['id' => $otherDumbObj->foreign->id],
                'created_at' => null,
                'other_attr' => 'other_val'
            ],
            $otherDumbObj->toArray()
        );
    }

    // Implements JsonSerializable
    public function testJsonSerialize()
    {
        $otherDumbObj = $this->arrayableJsonSerializableDumbObj();

        // Timezone settings are likely to mess with this test, be my guest
        $reset_timezone = date_default_timezone_get();
        date_default_timezone_set('UTC');
        $otherDumbObj->created_at = new Carbon('2019-01-01 01:02:03');
        date_default_timezone_set($reset_timezone);

        $this->assertJsonStringEqualsJsonString(
            json_encode([
                'id' => null,
                'foreign_id' => $otherDumbObj->foreign->id,
                'foreign' => ['id' => $otherDumbObj->foreign->id],
                'created_at' => '2019-01-01T01:02:03+00:00',
                'other_attr' => 'other_val'
            ]),
            json_encode($otherDumbObj)
        );
    }

    // Implements Jsonable
    public function testToJson()
    {
        $otherDumbObj = new class extends TableModel {
            public static $table = 'other_dumbs';
            public static $attributes = [
                'id' => [],
                'attr' => []
            ];
        };
        $otherDumbObj->attr = '<>';

        $this->assertJsonStringEqualsJsonString(
            json_encode(['id' => null, 'attr' => '<>']),
            $otherDumbObj->toJson()
        );

        $this->assertJsonStringEqualsJsonString(
            json_encode(['id' => null, 'attr' => '<>'], JSON_HEX_TAG),
            $otherDumbObj->toJson(JSON_HEX_TAG)
        );
    }

    public function testForeignObjectGetter()
    {
        $foreignObj = new ForeignTableModel();
        $foreignObj->insert();

        $dumbObj = new DumbTableModel(['foreign_id' => $foreignObj->id]);
        $dumbObj->insert();

        $this->expectExceptionMessage('Undefined property via __get()');

        try {
            $dumbObj->foreign;
        } finally {
            $this->assertNotNull($dumbObj->foreignObjectGetter('foreign'));
            $this->assertInstanceOf(ForeignTableModel::class, $dumbObj->foreign);
            $this->assertEquals(
                $dumbObj->foreign,
                $dumbObj->foreignObjectGetter('foreign')
            );
        }
    }

    public function testSelectWithoutAttributes()
    {
        $this->dumbObj->insert();
        $this->dumbObj->id = null; // This is actually a trick
        $this->dumbObj->insert();

        $dumbSelect = DumbTableModel::select();
        $this->assertSame(2, $dumbSelect->count());

        $modelAttrKeys = array_keys(DumbTableModel::$attributes);
        $selectAttrKeys = array_keys((array)$dumbSelect->first());
        $this->assertEqualsCanonicalizing($modelAttrKeys, $selectAttrKeys);
    }

    public function testSelectWithValidAttributes()
    {
        $this->dumbObj->insert();
        $dumbSelect = DumbTableModel::select(['id', 'attr1']);
        $this->assertSame(2, count(array_keys((array)$dumbSelect->first())));
    }

    public function testSelectWithDeleted()
    {
        $this->dumbObj->insert();
        $this->dumbObj->delete();
        $this->assertSame(0, DumbTableModel::select()->count());
        $this->assertSame(1, DumbTableModel::select(null, true)->count());
    }


    public function testSelectWithInvalidAttributes()
    {
        $this->dumbObj->insert();
        $dumbSelect = DumbTableModel::select(['nonexistent_attr']);
        $this->expectException(\Illuminate\Database\QueryException::class);
        $dumbSelect->first();
    }

    public function testFindByPrimaryKey()
    {
        $this->dumbObj->insert();
        $id = $this->dumbObj->id;
        $foundObj = DumbTableModel::find($id);
        $this->assertInstanceOf(DumbTableModel::class, $foundObj);
        $this->assertSame($id, $foundObj->id);
    }

    public function testFindByNonPrimaryKey()
    {
        $this->dumbObj->attr1 = 'rock';
        $this->dumbObj->insert();
        $foundObj = DumbTableModel::find('rock', 'attr1');
        $this->assertInstanceOf(DumbTableModel::class, $foundObj);
        $this->assertSame('rock', $foundObj->attr1);
    }

    public function testFindByMultipleFields()
    {
        $this->dumbObj->attr1 = 'rocktor';
        $this->dumbObj->insert();
        $foundObj = DumbTableModel::find('rocktor', ['id', 'attr1']);
        $this->assertInstanceOf(DumbTableModel::class, $foundObj);
        $this->assertSame('rocktor', $foundObj->attr1);
    }

    public function testFindByInvalidField()
    {
        $this->expectException(\Illuminate\Database\QueryException::class);
        $foundObj = DumbTableModel::find('lasglbasd', 'nonexistent_attr');
    }

    public function testFindByInvalidValue()
    {
        $this->expectException(
            \Hawese\Core\Exceptions\ModelObjectNotFoundException::class
        );
        $foundObj = DumbTableModel::find('nonexistent_val', 'attr1');
    }

    public function testInsertWithAutoincrement()
    {
        $this->assertIsInt($this->dumbObj->insert());
        $this->assertNotNull($this->dumbObj->id);
        $this->assertNotNull($this->dumbObj->created_at);
    }

    public function testInsertWithoutAutoincrement()
    {
        static::schema()->create(
            'other_dumbs',
            function (\Illuminate\Database\Schema\Blueprint $table) {
                $table->string('non_auto_id')->primary();
            }
        );

        $otherDumbObj = new class extends TableModel {
            public static $table = 'other_dumbs';
            public static $attributes = ['non_auto_id' => []];
            public static $primary_key = 'non_auto_id';
            protected static $incrementing = false;
        };

        $otherDumbObj->non_auto_id = 'string';

        $this->assertSame('string', $otherDumbObj->insert());
        $this->assertSame(1, $otherDumbObj->select()->count());

        static::schema()->drop('other_dumbs');
    }

    public function testUpdateAllFields()
    {
        $this->dumbObj->insert();
        $this->assertNull($this->dumbObj->updated_at);

        $this->dumbObj->attr1 = 'value1';
        $this->dumbObj->attr2 = 'value2';

        $this->assertTrue($this->dumbObj->update());

        $dumbObjFromDb = DumbTableModel::find($this->dumbObj->id);

        $this->assertSame('value1', $dumbObjFromDb->attr1);
        $this->assertSame('value2', $dumbObjFromDb->attr2);
        $this->assertNotNull($dumbObjFromDb->updated_at);
    }

    public function testUpdateSpecificFields()
    {
        $this->dumbObj->insert();
        $this->assertNull($this->dumbObj->updated_at);

        $this->dumbObj->attr1 = 'value1';
        $this->dumbObj->attr2 = 'value2';

        $this->assertTrue($this->dumbObj->update(['attr2']));

        $dumbObjFromDb = DumbTableModel::find($this->dumbObj->id);

        $this->assertNull($dumbObjFromDb->attr1);
        $this->assertSame('value2', $dumbObjFromDb->attr2);
        $this->assertNotNull($dumbObjFromDb->updated_at);
    }

    public function testUpdateSpecificInvalidFields()
    {
        $this->dumbObj->insert();
        $this->expectExceptionMessage('Undefined index');
        $this->dumbObj->update(['nonexistent_attr']);
    }

    public function testDelete()
    {
        $this->dumbObj->insert();
        $id = $this->dumbObj->id;

        $this->assertNotNull(DumbTableModel::find($id)->id);
        $this->assertTrue($this->dumbObj->delete());

        $this->expectException(
            \Hawese\Core\Exceptions\ModelObjectNotFoundException::class
        );
        DumbTableModel::find($id);
    }

    public function testStaticDeleteWithDeletedAt()
    {
        $ids_to_delete = [];
        $id_to_keep = $this->dumbObj->insert(); // don't delete this id

        $this->dumbObj->id = null;
        $ids_to_delete[] = $this->dumbObj->insert();
        $this->dumbObj->id = null;
        $ids_to_delete[] = $this->dumbObj->insert();

        $this->assertTrue(DumbTableModel::staticDelete($ids_to_delete));
        $this->assertSame(1, DumbTableModel::select()->count());
        $this->assertSame($id_to_keep, DumbTableModel::select()->first()->id);
    }

    public function testStaticDeleteWithoutDeletedAt()
    {
        $modelWithoutDeletedAt = new ForeignTableModel();
        $modelWithoutDeletedAt->insert();
        $this->assertSame(1, ForeignTableModel::select()->count());
        $this->assertTrue(
            ForeignTableModel::staticDelete([$modelWithoutDeletedAt->id])
        );
        $this->assertSame(0, ForeignTableModel::select()->count());
    }

    public function testValidate()
    {
        $this->dumbObj->attr1 = 'string_value';
        $this->dumbObj->validate();
        $this->dumbObj->attr1 = 1;
        $this->expectException(
            \Hawese\Core\Exceptions\ModelValidationException::class
        );
        $this->dumbObj->validate();
    }

    // TODO: Get rid of the TableModelCollection class, I don't like it at all
    public function testProcessCollection()
    {
        for ($i = 0; $i < 10; $i++) {
            $this->dumbObj->id = null;
            $this->dumbObj->insert();
        }

        $dumbCollection = DumbTableModel::select()->get();
        $this->assertSame(10, $dumbCollection->count());
        $dumbObjCollection = DumbTableModel::processCollection($dumbCollection);
        $this->assertSame(10, $dumbObjCollection->get()->count());
        $this->assertInstanceOf(
            DumbTableModel::class,
            $dumbObjCollection->get()->first()
        );
    }

    public function testAttributes()
    {
        $this->assertEqualsCanonicalizing(
            array_keys(DumbTableModel::$attributes),
            DumbTableModel::attributes()
        );
    }

    public function testInstanceAttributes()
    {
        $this->dumbObj->appendInstanceAttribute('new_attr');
        $this->assertEqualsCanonicalizing(
            array_merge(DumbTableModel::attributes(), ['new_attr']),
            $this->dumbObj->instanceAttributes()
        );
    }

    public function testForeignKeys()
    {
        $this->assertEqualsCanonicalizing(
            array_keys(DumbTableModel::$foreign_keys),
            DumbTableModel::foreignKeys()
        );
    }

    public function testGuessFk()
    {
        $this->assertSame(
            'foreign_id',
            DumbTableModel::guessFk('foreign')
        );

        $this->expectException(
            \Hawese\Core\Exceptions\UnknownForeignObjectException::class
        );
        DumbTableModel::guessFk('unknown');
    }

    public function testAppendInstanceAttribute()
    {
        $this->dumbObj->appendInstanceAttribute('new_attr');
        $this->assertContains('new_attr', $this->dumbObj->instanceAttributes());
    }

    public function testSnakeToPascalCase()
    {
        $this->assertSame(
            'HelloWorldImAPhrase',
            DumbTableModel::snakeToPascalCase('hello_world_im_a_phrase')
        );
    }

    public function testCommaStrRepeat()
    {
        $this->assertSame('', DumbTableModel::commaStrRepeat('?', 0));
        $this->assertSame('?,?,?,?,?', DumbTableModel::commaStrRepeat('?', 5));
    }
}
